package is.glif.fenius.client.connection;

import is.glif.fenius.client.connection.beans.ResServerInfo;
import is.glif.fenius.client.connection.beans.ServerInfo;
import is.glif.fenius.client.connection.beans.YAMLListReq;
import is.glif.fenius.client.connection.beans.YAMLQueryReq;
import is.glif.fenius.client.connection.beans.YAMLQueryReqList;
import is.glif.fenius.client.connection.beans.YAMLReleaseReq;
import is.glif.fenius.client.connection.beans.YAMLResv;
import is.glif.fenius.client.connection.beans.YAMLResvList;
import is.glif.fenius.util.ConfigManager;
import is.glif.fenius.ws.connection.gen.Advance;
import is.glif.fenius.ws.connection.gen.Avp;
import is.glif.fenius.ws.connection.gen.AvpSeq;
import is.glif.fenius.ws.connection.gen.Connection;
import is.glif.fenius.ws.connection.gen.ConnectionService;
import is.glif.fenius.ws.connection.gen.Edge;
import is.glif.fenius.ws.connection.gen.ExternalFault_Exception;
import is.glif.fenius.ws.connection.gen.InternalFault_Exception;
import is.glif.fenius.ws.connection.gen.ListReq;
import is.glif.fenius.ws.connection.gen.Query;
import is.glif.fenius.ws.connection.gen.QueryReq;
import is.glif.fenius.ws.connection.gen.QueryResponse;
import is.glif.fenius.ws.connection.gen.Release;
import is.glif.fenius.ws.connection.gen.ReleaseReq;
import is.glif.fenius.ws.connection.gen.Reservable;
import is.glif.fenius.ws.connection.gen.Reservation;
import is.glif.fenius.ws.connection.gen.Reserve;
import is.glif.fenius.ws.connection.gen.ReserveReq;
import is.glif.fenius.ws.connection.gen.ReserveResp;
import is.glif.fenius.ws.connection.gen.ReserveResponse;
import is.glif.fenius.ws.connection.gen.Schedule;
import is.glif.fenius.ws.connection.gen.Topology;
import is.glif.fenius.ws.connection.gen.Vertex;
import is.glif.fenius.ws.connection.gen.ListResponse;

import java.io.FileInputStream;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;

import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.security.FiltersType;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.log4j.Logger;

import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;

public class Requester {
    private static Logger log = Logger.getLogger(Requester.class);
    private static Requester instance;
    private String sslTruststorePath;
    private String sslKeystorePath;
    private String sslTruststorePass;
    private String sslKeystorePass;
    private String sslKeyPass;
    private boolean sslCheckHostnames;
    private Long httpTimeoutMs;
    private Map<String, Object> config;
    private static final QName SERVICE_NAME = new QName("http://fenius.glif.is/ws/connection/v1/service", "connection");

    
    public static Requester getInstance() {
        if (instance == null) {
            instance = new Requester();
        }
        return instance;
        
    }
 
    private Requester() {
        configure();
    }

    private void configure() {
        ConfigManager cm = ConfigManager.getInstance();
        this.config = cm.getConfiguration();
        
        this.sslKeystorePath = (String) this.config.get("sslKeystorePath");
        this.sslKeystorePass = (String) this.config.get("sslKeystorePass");
        this.sslKeyPass = (String) this.config.get("sslKeyPass");
        this.sslTruststorePath = (String) this.config.get("sslTruststorePath");
        this.sslTruststorePass = (String) this.config.get("sslTruststorePass");
        
        this.sslCheckHostnames = (Boolean) this.config.get("sslCheckHostnames");
        Integer timeoutMs = (Integer) this.config.get("httpTimeoutMs");
        this.httpTimeoutMs = timeoutMs.longValue();
        log.debug("keystore path:"+sslKeystorePath);
        log.debug("keystore pass:"+sslKeystorePass);
        log.debug("truststore path:"+sslTruststorePath);
        log.debug("truststore pass:"+sslTruststorePass);
    }
    
    public void reserve() throws ClientException {
        ArrayList<ResServerInfo> rsis = this.loadResvRequests();
        
        for (ResServerInfo rsi : rsis) {
            System.out.println(rsi);
            Reservable res = rsi.getReservable();
            ServerInfo si = rsi.getServerInfo();
            String url = si.getUrl();
            String user = si.getHttpUsername();
            String pwd = si.getHttpPassword();
            boolean ssl = si.isUseSSL();
            boolean httpBasic = si.isUseHttpBasicAuth();
            ConnectionService cs = this.initSoap(url, user, pwd, httpBasic, ssl);
            this.sendReserve(cs, res);
        }
        
    }
    public void list() throws ClientException {
        ConfigHolder h = ConfigHolder.getInstance();
        for (YAMLListReq listReq : h.getListReqs().getListReqs()) {
            String server = listReq.getServer();
            log.debug("sending list() to :"+server);
            ServerInfo si = h.getServers().getServerInfo(server);
            String url = si.getUrl();
            String user = si.getHttpUsername();
            String pwd = si.getHttpPassword();
            boolean ssl = si.isUseSSL();
            boolean httpBasic = si.isUseHttpBasicAuth();
            ConnectionService cs = this.initSoap(url, user, pwd, httpBasic, ssl);
            List<Reservation> resvs = this.sendList(cs);
            for (Reservation resv : resvs) {
                System.out.println(StringOutputter.printReservation(resv));
            }
        }
    }
    public void query() throws ClientException {
        ConfigHolder h = ConfigHolder.getInstance();
        for (YAMLQueryReq queryReq : h.getQueryReqs().getQueryReqs()) {
            String server = queryReq.getServer();
            String resvId = queryReq.getResvId();
            log.debug("sending query() for "+resvId+" to :"+server);
            ServerInfo si = h.getServers().getServerInfo(server);
            String url = si.getUrl();
            String user = si.getHttpUsername();
            String pwd = si.getHttpPassword();
            boolean ssl = si.isUseSSL();
            boolean httpBasic = si.isUseHttpBasicAuth();
            ConnectionService cs = this.initSoap(url, user, pwd, httpBasic, ssl);
            Reservation resv = this.sendQuery(cs, resvId);
            System.out.println(StringOutputter.printReservation(resv));
        }
    }
    public void release() throws ClientException {
        ConfigHolder h = ConfigHolder.getInstance();
        for (YAMLReleaseReq relReq : h.getReleaseReqs().getReleaseReqs()) {
            String server = relReq.getServer();
            log.debug("release() to :"+server);
            ServerInfo si = h.getServers().getServerInfo(server);
            String url = si.getUrl();
            String user = si.getHttpUsername();
            String pwd = si.getHttpPassword();
            boolean ssl = si.isUseSSL();
            boolean httpBasic = si.isUseHttpBasicAuth();
            ConnectionService cs = this.initSoap(url, user, pwd, httpBasic, ssl);
            
            for (String resvId : relReq.getResvIds()) {
                log.debug("  releasing resv: "+resvId);
                this.sendRelease(cs, resvId);
                log.debug("  .. done");
            }
        }
    }
    
    private ArrayList<ResServerInfo> loadResvRequests() {
        ArrayList<ResServerInfo> rsis = new ArrayList<ResServerInfo>();
        ConfigHolder h = ConfigHolder.getInstance();
        
        try {
            YAMLResvList yrl = h.getResvs();
            YAMLResv[] yrll = yrl.getResvs();
            for (YAMLResv yresv : yrll) {
                ResServerInfo res = this.makeResServerInfo(yresv);
                rsis.add(res);
            }
        } catch (ClassCastException ex) {
            log.error(ex);
            ex.printStackTrace();
            return rsis;
        }
        return rsis;
    }
    
    private ResServerInfo makeResServerInfo(YAMLResv resv) {
        ResServerInfo rsi = new ResServerInfo();
        ConfigHolder h = ConfigHolder.getInstance();
        String server = resv.getServer();
        ServerInfo si = h.getServers().getServerInfo(server);
        Reservable res = new Reservable();
        rsi.setServerInfo(si);
        
        
        // set topology
        Topology topo = new Topology();
        topo.setDirected(false);

        Vertex a = new Vertex();
        a.setVertexId("vertexA");
        a.setResourceId(resv.getA());
        Vertex z = new Vertex();
        z.setVertexId("vertexZ");
        z.setResourceId(resv.getZ());

        topo.getVertices().add(a);
        topo.getVertices().add(z);
        
        Edge e = new Edge();
        e.setA("vertexA");
        e.setBandwidth(resv.getBandwidth());
        e.setZ("vertexZ");
        topo.getEdges().add(e);
        
        res.setTopology(topo);

        
        // set ethernet params
        Avp srcVlanValuePair = new Avp();
        srcVlanValuePair.setAttrName("eth.vlanTag");
        srcVlanValuePair.setAttrVal(resv.getaVlan().toString());
        AvpSeq aTechParams = new AvpSeq();
        aTechParams.getAvp().add(srcVlanValuePair);
        a.setTechParams(aTechParams);
        
        
        Avp dstVlanValuePair = new Avp();
        dstVlanValuePair.setAttrName("eth.vlanTag");
        dstVlanValuePair.setAttrVal(resv.getzVlan().toString());
        AvpSeq zTechParams = new AvpSeq();
        zTechParams.getAvp().add(dstVlanValuePair);
        z.setTechParams(zTechParams);
        
        
        Integer duration = resv.getDuration();
        String start = resv.getStart().trim();
        Integer offset = 0;
        if (start.equals("now")) {
            
        } else {
            try {
                offset = Integer.parseInt(start);
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                System.exit(1);
            }
            if (offset < 0) {
                System.err.println("start offset must be positive");
                System.exit(1);
            }
        }
        GregorianCalendar gcNow = (GregorianCalendar) GregorianCalendar.getInstance();
        GregorianCalendar gcCal = (GregorianCalendar) GregorianCalendar.getInstance();
        Long ms = gcCal.getTimeInMillis();
        ms += offset * 1000;
        gcCal.setTimeInMillis(ms);
        XMLGregorianCalendar xmlNow = new XMLGregorianCalendarImpl(gcNow);
        XMLGregorianCalendar xmlCal = new XMLGregorianCalendarImpl(gcCal);

        Advance adv = new Advance();
        adv.setStart(xmlCal);
        adv.setDuration(duration);

        Schedule sched = new Schedule();
        sched.setAdvance(adv);
        sched.setCreated(xmlNow);
        res.setSchedule(sched);

        
        res.getDescription().add("Fenius reservation");        
        rsi.setReservable(res);
        return rsi;
    }
    
    private void sendRelease(ConnectionService port, String resvId) {
        Release rel = new Release();
        ReleaseReq req = new ReleaseReq();
        req.setReservationId(resvId);
        rel.setArg0(req);
        try {
            port.release(rel);
        } catch (InternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        }
    }
    
    private Reservation sendQuery(ConnectionService port, String resvId) {
        Query q = new Query();
        QueryReq req = new QueryReq();
        q.setArg0(req);
        req.setReservationId(resvId);
        
        try {
            QueryResponse resp = port.query(q);
            Reservation resv = resp.getReturn().getReservation();
            return (resv);
        } catch (InternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        }
        return null;
    }
    private Reservation sendReserve(ConnectionService port, Reservable res) {
        Reserve reserve = new Reserve();
        ReserveReq req = new ReserveReq();
        req.setReservationParams(res);
        reserve.setArg0(req);
        try {
            ReserveResponse resp = port.reserve(reserve);
            ReserveResp rsv = resp.getReturn();
            return (rsv.getReservation());
        } catch (InternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            log.error(e);
            e.printStackTrace();
        }
        return null;
    }
    
    private List<Reservation> sendList(ConnectionService port) {
        List<Reservation> result = new ArrayList<Reservation>();
        is.glif.fenius.ws.connection.gen.List params = new is.glif.fenius.ws.connection.gen.List();
        ListReq lr = new ListReq();
        params.setArg0(lr);
        try {
            ListResponse resp = port.list(params);
            result = resp.getReturn().getReservations();
        } catch (InternalFault_Exception e) {
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            e.printStackTrace();
        }
        return result;
        
    }
    
    
    private ConnectionService initSoap(String url, String user, String pwd, boolean httpBasic, boolean ssl) throws ClientException {
        String wsdlLocation = url;
        URL wsdlURL = null;
        try {
            wsdlURL = new URL(wsdlLocation);
        } catch (MalformedURLException ex) {
            log.error(ex);
            throw new ClientException("Internal error");
        }
        if (ssl) {
            log.info("ssl enabled for "+url);
            configureSSLinJVM(wsdlURL);
        }
        
        // before 
        if (!sslCheckHostnames) {
            HttpsURLConnection.setDefaultHostnameVerifier(new NullHostnameVerifier());
        }
        if (httpBasic) {
            Authenticator.setDefault(new MyAuthenticator(user, pwd));        
        }
        
        System.out.println("wsdl: "+wsdlLocation);
        Connection ss = new Connection(wsdlURL, SERVICE_NAME);
        log.debug("-------> initialized proxy");
        
        
        ConnectionService port = ss.getConnectionServicePort();
        org.apache.cxf.endpoint.Client client = ClientProxy.getClient(port); 
        HTTPConduit httpConduit = (HTTPConduit) client.getConduit();

        HTTPClientPolicy cp = new HTTPClientPolicy();
        Long connTO = httpTimeoutMs;
        Long rcvTO = httpTimeoutMs;
        
        cp.setConnectionTimeout(connTO);
        cp.setReceiveTimeout(rcvTO);
        httpConduit.setClient(cp);
        
        if (ssl) {
            TLSClientParameters tlsParams = new TLSClientParameters(); 
            if (sslCheckHostnames) {
                log.info("checking CNs");
                tlsParams.setDisableCNCheck(false); 
            } else {
                log.info("not checking CNs");
                tlsParams.setDisableCNCheck(true); 
            }
            FiltersType filter = new FiltersType(); 
            filter.getInclude().add(".*_EXPORT_.*"); 
            filter.getInclude().add(".*_EXPORT1024_.*"); 
            filter.getInclude().add(".*_WITH_DES_.*"); 
            filter.getInclude().add(".*_WITH_3DES_.*"); 
            filter.getInclude().add(".*_WITH_NULL_.*"); 
            filter.getExclude().add(".*_DH_anon_.*"); 
            tlsParams.setCipherSuitesFilter(filter); 
            
            httpConduit.setTlsClientParameters(tlsParams);             
        }
        if (httpBasic) {
            BindingProvider bp = (BindingProvider) port;
            bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, user);
            bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, pwd);
        }
        log.debug("done");
        return port;
    }
    
    private void configureSSLinJVM(URL url) {
        log.debug("truststore: "+sslTruststorePath);
        System.setProperty("javax.net.ssl.trustStoreType", "jks");  
        System.setProperty("javax.net.ssl.trustStore", sslTruststorePath);  
        System.setProperty("javax.net.ssl.trustStorePassword", sslTruststorePass);  
          
        System.setProperty("javax.net.ssl.keyStoreType", "jks");  
        System.setProperty("javax.net.ssl.keyStore", sslKeystorePath);  
        System.setProperty("javax.net.ssl.keyStorePassword",sslKeystorePass);  
        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            char[] keyStorePassword = sslKeystorePass.toCharArray();
            char[] privateKeyPassword = sslKeyPass.toCharArray();
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(new FileInputStream(sslKeystorePath), keyStorePassword);
            kmf.init(ks, privateKeyPassword);
            ctx.init(kmf.getKeyManagers(), null, null);
            SSLSocketFactory sf = ctx.getSocketFactory();
            HttpsURLConnection.setDefaultSSLSocketFactory(sf);
            SSLSocket socket = (SSLSocket) sf.createSocket(url.getHost(), url.getPort());
            String[] suites = socket.getSupportedCipherSuites();
            socket.setEnabledCipherSuites(suites);
            
        } catch (Exception ex) {
            log.error(ex);
        }
    }
    private static class MyAuthenticator extends Authenticator {
        String user;
        String pwd;
        public MyAuthenticator(String user, String pwd) {
            this.user = user;
            this.pwd = pwd;
        }
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            log.debug("sending "+user+" "+pwd);
            return new PasswordAuthentication(user, pwd.toCharArray());
        }
    }
    private static class NullHostnameVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }
}
